/* Copyright © 2023 - 2024 Coremail论客
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import { getLogger } from "../utils/log";
import type { Connection } from "./network";
import { base64Encode, decodeCharset } from "../utils/encodings";
import { assert, delay, sCRLF } from "../utils/common";
import type { ICommand } from "./protocol_base";
import { ProtocolBase } from "./protocol_base";
import { CMError, ErrorCode, IBuffer } from "../api";
import { Tokenizer } from "./tokenizer";
import { MemBuffer, memBufferCreator } from '../utils/file_stream';
import { replaceBufferData } from '../utils/buffer_utils';

const logger = getLogger("protocol:smtp");

export enum SMTP_CODE {
  SERVICE_READY = 220,
  SERVICE_CLOSE = 221,
  AUTH_SUCCESS = 235,
  OK = 250,
  START_MAIL_INPUT = 354,
  AUTH_LOGIN = 334,
  AUTH_CANCEL = 501,
  AUTH_ERROR = 535,
  COMMAND_ERROR = 500,
  COMMAND_PARAM_ERROR = 501,
  COMMAND_SEQ_ERROR = 503,
  MAILBOX_ERROR = 550,
  TRANSACTION_FAILED = 554,
}

export type SmtpResponse = {
  code: SMTP_CODE;
  message: string[];
};

async function getResponse(conn: Connection): Promise<SmtpResponse> {
  const tokenizer = new Tokenizer(conn);
  await tokenizer.feed();
  logger.trace('oauth tokenizer', tokenizer.s, tokenizer.i);
  const code = tokenizer.getNumber();
  const content: string[] = [];
  let ch = tokenizer.peekNextChar();
  while (ch == "-") {
    tokenizer.expect("-");
    let line = tokenizer.getUntil(sCRLF);
    content.push(line);
    await tokenizer.feed();
    const code2 = tokenizer.getNumber();
    assert(code2 == code);
    ch = tokenizer.peekNextChar();
  }
  let line = tokenizer.getUntil(sCRLF);
  content.push(line);
  return { code, message: content };
}

const MAX_SIZE_TO_SEND = 1024 * 100;
class SMTPCommand implements ICommand<SmtpResponse> {
  data: string[] = [];
  ok: boolean = true;
  command: string | IBuffer;
  constructor(command: string | IBuffer) {
    this.command = command;
  }

  async do(conn: Connection): Promise<SmtpResponse> {
    if (typeof this.command == 'string') {
      await conn.send(this.command);
    } else if (this.command instanceof MemBuffer) {
      for (const chunk of this.command.data) {
        await conn.send(chunk);
      }
    } else {
      for await (const chunk of this.command.readRaw()) {
        await conn.send(chunk);
      }
    }
    await conn.send(sCRLF);
    const resp = await getResponse(conn);

    return resp;
  }
}

export type SMTP_Extention = {
  name: string;
};

const Replace = new Uint8Array([13, 10, 0x2e, 0x2e]);
const Pattern = new Uint8Array([13, 10, 0x2e]);

export class SmtpProtocol extends ProtocolBase<SmtpResponse> {
  constructor(host: string, port: number, isSSL: boolean, ca: string[] = []) {
    super(host, port, isSSL, ca);
  }

  async onConnected(): Promise<void> {
    const resp = await getResponse(this.conn);
    logger.debug("onConnected", resp);
  }

  async auth(user: string, pass: string): Promise<SmtpResponse> {
    let res = await this.doCommand(new SMTPCommand("AUTH LOGIN"));
    if (res.code != SMTP_CODE.AUTH_LOGIN) {
      return res;
    }
    res = await this.doCommand(new SMTPCommand(base64Encode(user)));
    if (res.code != SMTP_CODE.AUTH_LOGIN) {
      return res;
    }
    res = await this.doCommand(new SMTPCommand(base64Encode(pass)));
    if (res.code != SMTP_CODE.AUTH_LOGIN) {
      return res;
    }
    return res;
  }

  async oauth2(encodedToken: string): Promise<SmtpResponse> {
    logger.trace('oauth2', encodedToken);
    let res = await this.doCommand(new SMTPCommand("AUTH XOAUTH2"));
    logger.trace('oauth2 xoauth2', res);
    if (res.code != SMTP_CODE.AUTH_LOGIN) {
      return res;
    }
    res = await this.doCommand(new SMTPCommand(encodedToken));
    logger.trace('oauth2 sent token', res);
    return res;
  }

  mail(email: string): Promise<SmtpResponse> {
    return this.doCommand(new SMTPCommand(`MAIL FROM: <${email}>`));
  }

  rcpt(email: string): Promise<SmtpResponse> {
    return this.doCommand(new SMTPCommand(`RCPT TO: <${email}>`));
  }

  async data(data: string | IBuffer): Promise<SmtpResponse> {
    let res = await this.doCommand(new SMTPCommand(`DATA`));
    logger.debug("data", res);
    if (res.code == SMTP_CODE.START_MAIL_INPUT) {
      if (typeof data == 'string') {
        res = await this.doCommand(
          new SMTPCommand(data.replace(/\r\n\./g, "\r\n..") + "\r\n.")
        );
      } else {
        const b = memBufferCreator.createBuffer({});
        if (data instanceof MemBuffer) {
          for (const chunk of data.data) {
            if (typeof chunk == 'string') {
              b.feed(chunk.replace(/\r\n\./g, "\r\n.."));
            } else {
              b.feed(chunk);
            }
          };
        } else {
          b.append(data);
        }
        b.end('\r\n.');
        res = await this.doCommand(
          new SMTPCommand(b)
        )
      }
    }
    return res;
  }

  helo(host: string): Promise<SmtpResponse> {
    return this.doCommand(new SMTPCommand(`HELO ${host}`));
  }

  ehlo(host: string): Promise<SmtpResponse> {
    return this.doCommand(new SMTPCommand(`EHLO ${host}`));
  }

  quit(): Promise<SmtpResponse> {
    return this.doCommand(new SMTPCommand(`QUIT`));
  }

  noop(): void {
  }
}
